package nl.ViewerServer.Webservice;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;

import javax.servlet.http.HttpServletRequest;

import nl.ViewerServer.Application;
import nl.ViewerServer.Database.Objects.ParameterValue;
import nl.ViewerServer.Exceptions.AuthorizationException;
import nl.ViewerServer.Exceptions.DataInitializationException;
import nl.ViewerServer.Exceptions.PrivilegeException;
import nl.ViewerServer.Exceptions.ServerException;
import nl.ViewerServer.Exceptions.UserRequestException;
import nl.ViewerServer.ServerManager.ServerManager;
import nl.ViewerServer.ServerManager.DataManager.DataObjects.DataObject;
import nl.ViewerServer.ServerManager.DataManager.Objects.Dataset;
import nl.ViewerServer.ServerManager.DataManager.Objects.Modality;
import nl.ViewerServer.ServerManager.DataManager.Objects.Output;
import nl.ViewerServer.ServerManager.DataManager.Objects.Parameter;
import nl.ViewerServer.ServerManager.DataManager.Objects.Visualization;
import nl.ViewerServer.ServerManager.UserManager.Objects.User;
import nl.ViewerServer.SessionManager.SessionManager;
import nl.ViewerServer.SessionManager.Objects.Job;
import nl.ViewerServer.SessionManager.Objects.Session;

import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.attachments.AttachmentPart;
import org.apache.axis.client.Call;
import org.apache.axis.transport.http.HTTPConstants;
import org.apache.log4j.Logger;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class Webservice {
   private static Logger  log            = Logger.getLogger( Webservice.class );
   private static boolean logDebugToInfo = true;

   public static void logDebug( String message, Throwable t ) {
      if( logDebugToInfo ) {
         log.info( "(DEBUG): " + message, t );
      } else {
         log.debug( message, t );
      }
   }

   public static void logDebug( String message ) {
      logDebug( message, null );
   }

   private static void handleException( Throwable t ) throws Exception {
      // extract the webservice method and the line number in the webservice class where the exception was raised,
      // to include in the error message (for example '<authorizeUser (:115)>' which means that Webservice.java:115
      // raised the exception, which was in method authorizeUser( .. ) )

      String exceptionLocation;
      if( t.getStackTrace().length > 0 ) {
         StackTraceElement ste = t.getStackTrace()[0];
         exceptionLocation = "<" + ste.getMethodName() + " (:" + ste.getLineNumber() + ")>";
      } else {
         // shouldn't happen
         exceptionLocation = "<unknown location>";
      }

      if( t instanceof AuthorizationException ) {
         log.warn( "Authorization failed for " + exceptionLocation + ": " + t.getMessage() );
         throw (AuthorizationException)t;
      } else if( t instanceof PrivilegeException ) {
         log.warn( "Insufficient privileges for " + exceptionLocation + ": " + t.getMessage() );
         throw (PrivilegeException)t;
      } else if( t instanceof UserRequestException ) {
         log.info( "User request failed for " + exceptionLocation + ": " + t.getMessage() );
         throw (UserRequestException)t;
      } else if( t instanceof ServerException ) {
         log.error( "Server exception for " + exceptionLocation + ": " + t.getMessage(), t );
         throw (ServerException)t;
      } else if( t instanceof IOException ) {
         log.error( "Server I/O error for " + exceptionLocation + ": " + t.getMessage(), t );

         // supply the client with a serverException without detailed information
         throw new ServerException( "An I/O error occurred on the server for " + exceptionLocation );
      } else if( t instanceof DataInitializationException ) {
         log.error( "DataObject initialization error for " + exceptionLocation + ": " + t.getMessage(), t );

         // supply the client with a serverException without detailed information
         throw new ServerException(
               "An error occurred on the server while accessing job result for " + exceptionLocation );
      } else if( t instanceof SQLException ) {
         log.error( "Database error for " + exceptionLocation + ": " + t.getMessage(), t );

         log.error( "Database state: " + ( (SQLException)t ).getSQLState() );
         log.error( "     errorcode: " + ( (SQLException)t ).getErrorCode() );

         // supply the client with a serverException without detailed information
         throw new ServerException(
               "An error occurred on the server while accessing the database for " + exceptionLocation );
      }

      // otherwise, unknown exception/throwable, make sure user never sees the server-specific exception
      log.error( "Unexpected exception occurred in " + exceptionLocation + ": " + t.getMessage(), t );
      throw new ServerException( "Unexpected server error occurred" );
   }

   public void setSessionProgress( long sessionId, String algorithm, int progress ) {
      Session session = SessionManager.getInstance().getSession( sessionId );
      if( session == null ) {
         return; // ignore invalid session id
      }

      session.setProgress( algorithm, progress );
   }

   private static User authorizeUser() throws AuthorizationException, SQLException {
      return authorizeUser( false );
   }

   private static String getClientIP() {
      HttpServletRequest req = (HttpServletRequest)MessageContext.getCurrentContext().getProperty(
            HTTPConstants.MC_HTTP_SERVLETREQUEST );
      return req.getRemoteAddr();
   }

   private static User authorizeUser( boolean purgeCache ) throws AuthorizationException, SQLException {
      MessageContext msgContext = MessageContext.getCurrentContext();

      User u = null;
      if( msgContext != null ) {
         u = ServerManager.userManager.getUser( msgContext.getUsername(), msgContext.getPassword(), purgeCache );
      }
      if( u == null )
         throw new AuthorizationException( "Username and/or password incorrect" );
      return u;
   }

   /*
    * private static AugmentedUser authorizeUser( String privileges, boolean purgeCache ) throws AuthorizationException,
    * PrivilegeException { AugmentedUser u = authorizeUser( purgeCache );
    * 
    * if( !u.privilegeAllowed( privileges ) ) throw new PrivilegeException( "You are not allowed the privilege(s) '" +
    * privileges + "'" );
    * 
    * return u; }
    */
   public static int ping() {
      return log.hashCode();
   }

   /**
    * This method will create a session on the server for a given client (identified by an application instance
    * identifier). The session will run the visualization resulting from the chosen modality and visualization.
    * 
    * @param clientInstanceIdentifier
    * @param modalityId
    * @param visualizationId
    * @return
    * @throws Exception
    * @throws AuthorizationException
    * @throws ServerException
    */
   // UserRequestException
   public long createSession( String clientInstanceIdentifier, int modalityId, int visualizationId ) throws Exception,
         AuthorizationException, ServerException {
      try {
         long sessionId = SessionManager.getInstance().createSession( authorizeUser(), getClientIP(),
               clientInstanceIdentifier, modalityId, visualizationId, -1 );
         logDebug( "Session " + sessionId + " created for modality/visualization " + modalityId + "/" + visualizationId + ", from " + clientInstanceIdentifier );
         return sessionId;
      }
      catch( Throwable t ) {
         handleException( t );
         return -1; // just for the compiler
      }

   }

   /**
    * This method will create a session like the default createSession method, but the user may supply a requested
    * number of threads (which will be honored if not conflicting with other rules)
    * 
    * @param clientInstanceIdentifier
    * @param modalityId
    * @param visualizationId
    * @param requestedThreads
    * @return
    * @throws Exception
    * @throws AuthorizationException
    * @throws ServerException
    */
   // UserRequestException
   public long createSession( String clientInstanceIdentifier, int modalityId, int visualizationId, int requestedThreads )
         throws Exception, AuthorizationException, ServerException {
      try {
         long sessionId = SessionManager.getInstance().createSession( authorizeUser(), getClientIP(),
               clientInstanceIdentifier, modalityId, visualizationId, requestedThreads );

         logDebug( "Session " + sessionId + " created for modality/visualization " + modalityId + "/" + visualizationId + ", from " + clientInstanceIdentifier );
         return sessionId;
      }
      catch( Throwable t ) {
         handleException( t );
         return -1; // just for the compiler
      }

   }

   /**
    * This method will create session that is based on an earlier stored lookmark.
    * 
    * @param clientInstanceIdentifier
    * @param lookmarkId
    * @return
    * @throws Exception
    * @throws AuthorizationException
    * @throws ServerException
    */
   public long createSessionFromLookmark( String clientInstanceIdentifier, int lookmarkId ) throws Exception,
         AuthorizationException, ServerException {
      try {
         User u = authorizeUser();

         logDebug( "Creating session from lookmark " + lookmarkId + " from " + clientInstanceIdentifier );
         // find out which user/modality/visualization we are talking about
         int[] lookmarkIds = ServerManager.dataManager.getIdsForLookmark( lookmarkId );

         ArrayList<ParameterValue> parameterValues = ServerManager.dataManager.getParameterValuesForLookmark( u,
               lookmarkIds[0], lookmarkId );

         // the sessionManager will check the ACL's for all objects
         long sessionId = SessionManager.getInstance().createSession( u, getClientIP(), clientInstanceIdentifier,
               lookmarkIds[1], lookmarkIds[2], -1 );

         Session session = SessionManager.getInstance().getSession( sessionId );
         // now set all parameters from the lookmark in this session
         for( ParameterValue pv : parameterValues ) {
            logDebug( "Parameter : " + pv.parameterName + " = " + pv.value );
            Parameter p = session.getParameter( pv.parameterName, null );

            if( p != null )
               p.setParameterValue( pv.value );
            else
               logDebug( "Parameter " + pv.parameterName + " does not exist!" );
         }
         logDebug( "Session created with " + parameterValues.size() + " parameterValues" );
         return sessionId;
      }
      catch( Throwable t ) {
         handleException( t );
         return -1; // just for the compiler
      }

   }

   // UserRequestException, AuthorizationException, ServerException
   public void closeSession( long sessionId, boolean removeJobs ) throws Exception {
      try {
         logDebug( "Closing session: " + sessionId );
         SessionManager.getInstance().closeSession( authorizeUser(), sessionId, removeJobs );
         logDebug( "Session closed: " + sessionId );
      }
      catch( Throwable t ) {
         handleException( t );
      }
   }

   // UserRequestException, ServerException, AuthorizationException
   public long submitJob( long sessionId, String outputName, String[] inParameterNames, Object[] inParameterValues )
         throws Exception {

      try {
         // long start = System.currentTimeMillis();

         logDebug( "Client job received for session " + sessionId + ", with output " + outputName );

         // create the job (session manager will check the user, session info and parameters supplied)
         Job job = SessionManager.getInstance().createJob( authorizeUser(), sessionId, outputName, inParameterNames,
               inParameterValues );

         // wait (at most the RPCTimeoutMs) to send a reply, otherwise the user will have to start polling,
         // which is really inefficient for both server and client
         // job.waitForFinish( Application.config.RPCTimeoutMs - ( System.currentTimeMillis() - start ) );

         if( job.session.getStatus() == Session.Status.CLOSED ) {
            throw new UserRequestException( "The session was closed while running the job" );
         }
         logDebug( "Job submitted: " + job.jobId );
         return job.jobId;
      }
      catch( Throwable t ) {
         handleException( t );
         return -1;
      }
   }

   // AuthorizationException, ServerException, UserRequestException, DataObjectException
   public String getJobParameters( long jobId ) throws Exception {
      try {
         // check that the user is actually creator of the data object
         User au = authorizeUser();

         ArrayList<ParameterValue> jobPVs = ServerManager.dataManager.getParameterValuesForJob( au, jobId );

         // find the dataobject and verify that it was created by the user
         DataObject object = ServerManager.dataManager.getDataObjectForJob( au, jobId );
         if( object == null ) {
            throw new UserRequestException( "The specified job has no result data associated with it" );
         }

         // add the dataObject description to the parametervalue array so the client knows the bounds of
         // the dataobject, the amount of tiles, etc..
         jobPVs.add( new ParameterValue( "DataObject", object ) );

         String output = XMLUtil.ParameterValueArrayListToXML( jobPVs );
         // log.info( output );
         return output;
      }
      catch( Throwable t ) {
         handleException( t );
         return null;
      }
   }

   // get job data
   // AuthorizationException, ServerException, UserRequestException, DataObjectException
   public byte[] getDataPart( long jobId, int resolution, int horizontalPart, int verticalPart ) throws Exception {
      try {
         // find the dataobject and verify that it was created by the user
         DataObject object = ServerManager.dataManager.getDataObjectForJob( authorizeUser(), jobId );
         if( object == null ) {
            throw new UserRequestException( "The specified job has no result data associated with it" );
         }

         return object.getPart( resolution, horizontalPart, verticalPart );
      }
      catch( Throwable t ) {
         handleException( t );
         return null;
      }

      // don't catch possible UserRequestException's thrown by findDataObjectForJob
      // when the userId does not match the authorized user
   }

   // AuthorizationException, ServerException, UserRequestException, DataObjectException
   // get data part for a job as attachment
   public boolean getDataPartAttached( long jobId, int resolution, int horizontalPart, int verticalPart )
         throws Exception {
      try {
         // find the dataobject and verify that it was created by the user
         DataObject object = ServerManager.dataManager.getDataObjectForJob( authorizeUser(), jobId );
         if( object == null ) {
            throw new UserRequestException( "The specified job has no result data associated with it" );
         }

         byte[] data = object.getPart( resolution, horizontalPart, verticalPart );

         MessageContext msgContext = MessageContext.getCurrentContext();
         msgContext.setProperty( Call.ATTACHMENT_ENCAPSULATION_FORMAT, Call.ATTACHMENT_ENCAPSULATION_FORMAT_DIME );
         Message responseMessage = msgContext.getResponseMessage();
         AttachmentPart ap = (AttachmentPart)responseMessage.createAttachmentPart();

         ap.setContent( new ByteArrayInputStream( data ), "application/octet-stream" );
         responseMessage.addAttachmentPart( ap );
         return true;
      }
      catch( Throwable t ) {
         handleException( t );
         return false;
      }

   }

   // AuthorizationException, ServerException
   public String getDatasets() throws Exception {
      try {
         User au = authorizeUser();

         ArrayList<Dataset> ds = ServerManager.dataManager.getDatasets( au );

         XStream xstream = new XStream( new DomDriver() );
         xstream.alias( "Dataset", Dataset.class );
         xstream.alias( "Modality", Modality.class );
         xstream.alias( "Visualization", Visualization.class );
         xstream.alias( "Output", Output.class );
         String output = xstream.toXML( ds );

         return output;
      }
      catch( Throwable t ) {
         handleException( t );
         return null;
      }
   }

   // AuthorizationException, UserRequestException, ServerException
   public String getStatus( String clientInstanceIdentifier ) throws Exception {
      try {
         User user = authorizeUser();
         String clientIP = getClientIP();

         long start = System.currentTimeMillis();

         String statusMessage = SessionManager.getInstance().getStatus( user, clientIP, clientInstanceIdentifier );

         if( statusMessage == null ) {
            // there were no sessions to get status for
            return null;
         }

         while( ( System.currentTimeMillis() - start ) < Application.config.RPCTimeoutMs && statusMessage != null && statusMessage.length() == 0 ) {
            try {
               Thread.sleep( 50 );
            }
            catch( InterruptedException ie ) {
               // ignore
            }
            // re-check for status
            statusMessage = SessionManager.getInstance().getStatus( user, clientIP, clientInstanceIdentifier );
         }
         // to the client there should be no difference to the result in case of: 1) no valid sessions specified, and
         // 2) no altered sessions to return
         if( statusMessage == null || statusMessage.length() == 0 ) {
            return null;
         }
         return statusMessage;
      }
      catch( Throwable t ) {
         handleException( t );
         return null;
      }
   }

   // ServerException, UserRequestException, AuthorizationException
   public String getParametersForSession( long sessionId ) throws Exception {
      try {
         return getParametersForSession( sessionId, null );
      }
      catch( Throwable t ) {
         handleException( t );
         return null;
      }
   }

   // ServerException, UserRequestException, AuthorizationException
   public String getParametersForSession( long sessionId, String outputName ) throws Exception {
      try {
         ArrayList<Parameter> parameters = SessionManager.getInstance().getUserParametersForSession( authorizeUser(),
               sessionId, outputName );

         return XMLUtil.ParameterArrayListToXML( parameters );
      }
      catch( Throwable t ) {
         handleException( t );
         return null;
      }
   }

   public boolean createUserDefinedLookmark( long jobId, boolean isPersonal, String description ) throws Exception {
      try {
         return ServerManager.dataManager.createUserDefinedLookmark( authorizeUser(), jobId, isPersonal, description );
      }
      catch( Throwable t ) {
         handleException( t );
         return false;
      }
   }

   public String getLookmarks( int datasetId ) throws Exception {
      try {
         return XMLUtil.LookmarkInfoArrayListToXML( ServerManager.dataManager.getLookmarks( authorizeUser(), datasetId ) );
      }
      catch( Throwable t ) {
         handleException( t );
         return null;
      }
   }

}
